/*
 * QueryEngine.scala
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package org.squela
package sql
import java.sql.{Connection, Statement, ResultSet}

trait QueryEngine {
  private type StatementBlock[R] = CodeBlock[Statement, R]

  val syntaxBuilder: SqlSyntaxBuilder
  var uncatchedExceptionHandler: PartialFunction[Throwable, Unit] = {case ex: Throwable => throw ex}
  def getConnection(): Connection

  def withStatement[T](code: StatementBlock[T]): Option[T] = {withStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY , ResultSet.CLOSE_CURSORS_AT_COMMIT)(code)}
  def withStatement[T](resultSetType: Int = ResultSet.TYPE_FORWARD_ONLY, resultSetConcurrency: Int = ResultSet.CONCUR_READ_ONLY,
                       resultSetHoldability: Int = ResultSet.CLOSE_CURSORS_AT_COMMIT)(code: StatementBlock[T]): Option[T] = {
    val statement = getConnection().createStatement(resultSetType, resultSetConcurrency, resultSetHoldability)
    try {
      Some(code(statement))
    } catch {
      case t: Throwable =>
        if(code.catchClause != null && code.catchClause.isDefinedAt(t)) code.catchClause(t)
        else uncatchedExceptionHandler(t)
        None
    } finally {
      statement.close()
    }
  }
  def withPreparedStatement[T](query: String)(code: StatementBlock[T]): Option[T] = {withPreparedStatement(query, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY , ResultSet.CLOSE_CURSORS_AT_COMMIT)(code)}
  def withPreparedStatement[T](query: String, resultSetType: Int = ResultSet.TYPE_FORWARD_ONLY, resultSetConcurrency: Int = ResultSet.CONCUR_READ_ONLY,
                               resultSetHoldability: Int = ResultSet.CLOSE_CURSORS_AT_COMMIT)(code: StatementBlock[T]): Option[T] = {
    val statement = getConnection().prepareStatement(query, resultSetType, resultSetConcurrency, resultSetHoldability)
    try {
      Some(code(statement))
    } catch {
      case t: Throwable =>
        if(code.catchClause != null && code.catchClause.isDefinedAt(t)) code.catchClause(t)
        else uncatchedExceptionHandler(t)
        None
    } finally {
      statement.close()
    }
  }
  def withCallableStatement[T](query: String)(code: StatementBlock[T]): Option[T] = {withCallableStatement(query, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY , ResultSet.CLOSE_CURSORS_AT_COMMIT)(code)}
  def withCallableStatement[T](query: String, resultSetType: Int = ResultSet.TYPE_FORWARD_ONLY, resultSetConcurrency: Int = ResultSet.CONCUR_READ_ONLY,
                               resultSetHoldability: Int = ResultSet.CLOSE_CURSORS_AT_COMMIT)(code: StatementBlock[T]): Option[T] = {
    val statement = getConnection().prepareCall(query, resultSetType, resultSetConcurrency, resultSetHoldability)
    try {
      Some(code(statement))
    } catch {
      case t: Throwable =>
        if(code.catchClause != null && code.catchClause.isDefinedAt(t)) code.catchClause(t)
        else uncatchedExceptionHandler(t)
        None
    } finally {
      statement.close()
    }
  }
  def withinTransaction[T](code: CodeBlock[QueryEngine, T]): Option[T] = {
    val transactionedQE = new TransactionedQueryEngine(syntaxBuilder, uncatchedExceptionHandler, getConnection())
    try {
      Some(code(transactionedQE))
    } catch {
      case any: Throwable =>
        if (code.catchClause != null) code.catchClause(any)
        else uncatchedExceptionHandler(any)
        None
    }
  }
  def executeUpdate(query: Query) {
    withStatement(fun2CodeBlock{st: Statement => st executeUpdate(syntaxBuilder build query)})
  }
  def executeUpdate(query: Query, statement: Statement) {
    statement.executeUpdate(syntaxBuilder build query)
  }
  def executeQuery[T](query: Query)(code: QueryEngine.CodeBlock[QueryResult, T]): Option[T] = {
    withStatement({st: Statement =>
        code(new QueryResult(syntaxBuilder.syntaxProvider, query, st executeQuery(syntaxBuilder build query)))
      } onException(code.catchClause))
  }
  def executeQuery[T](query: Query, statement: Statement)(code: QueryEngine.CodeBlock[QueryResult, T]): Option[T] = {
    try {
      Some(code(new QueryResult(syntaxBuilder.syntaxProvider, query, statement executeQuery(syntaxBuilder build query))))
    } catch {
      case any: Throwable =>
        if(code.catchClause != null) code.catchClause(any)
        else uncatchedExceptionHandler(any)
        None
    }
  }

  private class TransactionedQueryEngine(val syntaxBuilder: SqlSyntaxBuilder,
                                         exceptionHandler: PartialFunction[Throwable, Unit],
                                         connection: Connection) extends QueryEngine {
    uncatchedExceptionHandler = exceptionHandler
    def getConnection() = connection
    override def withinTransaction[T](code: CodeBlock[QueryEngine, T]): Option[T] = {
      try {
        Some(code(this))
      } catch {
        case any: Throwable =>
          if (code.catchClause != null) code.catchClause(any)
          else uncatchedExceptionHandler(any)
          None
      }
    }
  }
}

object QueryEngine {

  private val BooleanClass = classOf[Boolean]
  private val ByteClass = classOf[Byte]
  private val ShortClass = classOf[Short]
  private val IntClass = classOf[Int]
  private val LongClass = classOf[Long]
  private val FloatClass = classOf[Float]
  private val DoubleClass = classOf[Double]
  private val CalendarClass = classOf[java.util.Calendar]

  class CodeBlock[T, R](val code: T => R) {
    def apply(st: T) = code(st)
    var catchClause: PartialFunction[Throwable, Unit] = null
    def onException(catchClause: PartialFunction[Throwable, Unit]) = {this.catchClause = catchClause; this}
  }

  class QueryResult(val syntaxProvider: SqlSyntaxProvider, val query: Query, val resultSet: ResultSet) extends Iterable[ResultTuple] {

    def iterator = new Iterator[ResultTuple] {
      def hasNext = resultSet.next()
      def next = {
        new ResultTuple(resultSet)
      }
    }

    /**
     * This method is very handy when constructing an object out of a select. Only those fields
     * that were selected will be setted to the objects.
     * Every object is created via the newInstance by-name parameter, then they are added to the
     * result after being poppulated with the fields from the select. Note that the bindings will
     * always have preference to the normal getters or setters from the object.
     */
    def applyTo[T <: AnyRef](newInstance: => T)(implicit bindings: Bindings = null): Seq[T] = {
      val res = new collection.mutable.ArrayBuffer[T]
      foreach {tuple =>
        val obj = newInstance
        query.queryType.asInstanceOf[Select[_]].fields foreach { fld =>
          if(bindings != null && bindings.contains(fld)) {
            bindings(fld).setter(obj, syntaxProvider.value2Type(tuple.get(fld), fld))
          } else {
            val scalaAssigner = fld.name + "_$eq"
            val javaAssigner = "set" + fld.name
            obj.getClass.getMethods find (m => (m.getName equalsIgnoreCase scalaAssigner) || (m.getName equalsIgnoreCase javaAssigner)) match {
              case Some(method) => method.invoke(obj, syntaxProvider.value2Type(tuple.get(fld), fld).asInstanceOf[AnyRef])
              case None =>
            }
          }
        }
        res += obj
      }
      res
    }
  }

  class ResultTuple private[QueryEngine] (rs: ResultSet) {

    def get[T](field: org.squela.Field[T]): T = get(field.name)(reflect.Manifest classType field.fieldType)

    def get[T: Manifest](columnIndex: Int) = {
      val t: T = manifest[T].erasure match {
        case BooleanClass => rs.getBoolean(columnIndex).asInstanceOf[T]
        case ByteClass => rs.getByte(columnIndex).asInstanceOf[T]
        case ShortClass => rs.getShort(columnIndex).asInstanceOf[T]
        case IntClass => rs.getInt(columnIndex).asInstanceOf[T]
        case LongClass => rs.getLong(columnIndex).asInstanceOf[T]
        case FloatClass => rs.getFloat(columnIndex).asInstanceOf[T]
        case DoubleClass => rs.getDouble(columnIndex).asInstanceOf[T]
        case CalendarClass =>
          val res = new java.util.GregorianCalendar
          val ts = rs.getTimestamp(columnIndex, res)
          res.setTimeInMillis(ts.getTime)
          res.asInstanceOf[T]
        case _ => rs.getObject(columnIndex).asInstanceOf[T]
      }
      t
    }
    def get[T: Manifest](columnName: String) =  {
      val t: T = manifest[T].erasure match {
        case BooleanClass => rs.getBoolean(columnName).asInstanceOf[T]
        case ByteClass => rs.getByte(columnName).asInstanceOf[T]
        case ShortClass => rs.getShort(columnName).asInstanceOf[T]
        case IntClass => rs.getInt(columnName).asInstanceOf[T]
        case LongClass => rs.getLong(columnName).asInstanceOf[T]
        case FloatClass => rs.getFloat(columnName).asInstanceOf[T]
        case DoubleClass => rs.getDouble(columnName).asInstanceOf[T]
        case CalendarClass =>
          val res = new java.util.GregorianCalendar
          val ts = rs.getTimestamp(columnName, res)
          res.setTimeInMillis(ts.getTime)
          res.asInstanceOf[T]
        case _ => rs.getObject(columnName).asInstanceOf[T]
      }
      t
    }
  }
}
